package pl.allegro.tech.hermes.management.infrastructure.query.parser.json;

import static java.util.stream.StreamSupport.stream;
import static pl.allegro.tech.hermes.management.infrastructure.query.MatcherQuery.fromMatcher;
import static pl.allegro.tech.hermes.management.infrastructure.utils.Iterators.stream;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import pl.allegro.tech.hermes.api.Query;
import pl.allegro.tech.hermes.management.infrastructure.query.matcher.AndMatcher;
import pl.allegro.tech.hermes.management.infrastructure.query.matcher.Matcher;
import pl.allegro.tech.hermes.management.infrastructure.query.matcher.MatcherFactories;
import pl.allegro.tech.hermes.management.infrastructure.query.matcher.MatcherNotFoundException;
import pl.allegro.tech.hermes.management.infrastructure.query.parser.Operator;
import pl.allegro.tech.hermes.management.infrastructure.query.parser.ParseException;
import pl.allegro.tech.hermes.management.infrastructure.query.parser.QueryParser;
import pl.allegro.tech.hermes.management.infrastructure.query.parser.QueryParserContext;

public class JsonQueryParser implements QueryParser, QueryParserContext {

  private static final String QUERY = "query";

  private final ObjectMapper objectMapper;

  public JsonQueryParser(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
  }

  @Override
  public <T> Query<T> parse(InputStream input, Class<T> type) {
    try {
      return parseDocument(objectMapper.readTree(input));
    } catch (IOException | MatcherNotFoundException e) {
      throw new ParseException("Query could not be parsed", e);
    }
  }

  @Override
  public <T> Query<T> parse(String query, Class<T> type) {
    return parse(new ByteArrayInputStream(query.getBytes()), type);
  }

  @Override
  public Matcher parseNode(JsonNode node) {
    Map.Entry<String, JsonNode> entry = singleNode(node);
    return parseSingleAttribute(entry.getKey(), entry.getValue());
  }

  @Override
  public List<Matcher> parseArrayNodes(JsonNode node) {
    if (!node.isArray()) {
      throw new ParseException("Element value was expected to be an array");
    }
    return parseObjectArray(node);
  }

  @Override
  public Object parseValue(JsonNode node) {
    if (!node.isValueNode()) {
      throw new ParseException("The node value wasn't present");
    }
    if (node.isTextual()) {
      return node.asText();
    } else if (node.isBoolean()) {
      return node.asBoolean();
    } else if (node.isInt()) {
      return node.asInt();
    } else if (node.isDouble()) {
      return node.asDouble();
    }
    return null;
  }

  @Override
  public Object[] parseArrayValue(JsonNode node) {
    if (!node.isArray()) {
      throw new ParseException("Element value was expected to be an array");
    }
    return stream(node.spliterator(), false).map(this::parseValue).toArray();
  }

  private <T> Query<T> parseDocument(JsonNode document) {
    validateDocument(document);
    return parseQuery(document.get(QUERY));
  }

  private <T> Query<T> parseQuery(JsonNode node) {
    return fromMatcher(parseCompoundObject(node), objectMapper);
  }

  private Matcher parseCompoundObject(JsonNode node) {
    return new AndMatcher(parseAllAttributes(node));
  }

  private <T> Matcher parseSingleAttribute(String key, JsonNode node) {
    if (isOperator(key)) {
      return parseOperator(key, node);
    } else if (node.isObject()) {
      return parseObject(key, node);
    } else {
      return parseAttribute(key, node);
    }
  }

  private <T> List<Matcher> parseAllAttributes(JsonNode node) {
    return stream(node.fields())
        .map(entry -> parseSingleAttribute(entry.getKey(), entry.getValue()))
        .collect(Collectors.toList());
  }

  private <T> Matcher parseOperator(String key, JsonNode node) {
    return MatcherFactories.getMatcherFactory(key).createMatcher(key, node, this);
  }

  private <T> Matcher parseObject(String key, JsonNode node) {
    Map.Entry<String, JsonNode> entry = singleNode(node);
    return MatcherFactories.getMatcherFactory(entry.getKey())
        .createMatcher(key, entry.getValue(), this);
  }

  private <T> List<Matcher> parseObjectArray(JsonNode node) {
    return stream(node.iterator()).map(this::parseCompoundObject).collect(Collectors.toList());
  }

  private <T> Matcher parseAttribute(String key, JsonNode node) {
    return MatcherFactories.defaultMatcher().createMatcher(key, node, this);
  }

  private boolean isOperator(String key) {
    return Operator.isValid(key);
  }

  private Map.Entry<String, JsonNode> singleNode(JsonNode node) {
    List<Map.Entry<String, JsonNode>> attributes = Lists.newArrayList(node.fields());
    if (attributes.size() != 1) {
      throw new MatcherNotFoundException(
          String.format(
              "The object must define exactly one member, but defines %s",
              Lists.newArrayList(node.fieldNames()).toString()));
    }
    return attributes.get(0);
  }

  private void validateDocument(JsonNode document) {
    if (!document.isObject() || !document.has(QUERY)) {
      throw new ParseException("JSON object must contain 'query' attribute");
    }
  }
}
